package com.xuzhiguang.lightnat.server.core.server.proxy;

import com.xuzhiguang.lightnat.server.core.client.NatClientService;
import com.xuzhiguang.lightnat.server.core.server.Server;
import com.xuzhiguang.lightnat.server.core.server.proxy.handler.*;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.bytes.ByteArrayEncoder;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Data
public class ProxyServer implements Server {

    private EventLoopGroup bossGroup;

    private EventLoopGroup workerGroup;

    private ServerBootstrap serverBootstrap;

    private ConcurrentHashMap<Long, Channel> channelMap = new ConcurrentHashMap<>();

    private NatClientService natClientService;

    private ProxyServer() {

    }

    public static class Builder {

        private final ProxyServer proxyServer;

        public Builder() {
            this.proxyServer = new ProxyServer();
        }

        public Builder natClientService(NatClientService natClientService) {
            this.proxyServer.setNatClientService(natClientService);
            return this;
        }

        public ProxyServer build() {
            return this.proxyServer;
        }
    }

    @Override
    public void start() throws InterruptedException {

        bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("p-boss"));
        workerGroup = new NioEventLoopGroup(new DefaultThreadFactory("p-worker"));
        serverBootstrap = new ServerBootstrap();

        ActiveHandler activeHandler = new ActiveHandler(this.natClientService);
        InactiveHandler inactiveHandler = new InactiveHandler();
        WritabilityChangedHandler writabilityChangedHandler = new WritabilityChangedHandler();
        TransferHandler transferHandler = new TransferHandler(this.natClientService);

        ExceptionHandler exceptionHandler = new ExceptionHandler();

        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_REUSEADDR, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast("byteArrayEncoder", new ByteArrayEncoder())
                                .addLast("idleCheckHandler", new IdleCheckHandler())
                                .addLast("activeHandler", activeHandler)
                                .addLast("inactiveHandler", inactiveHandler)
                                .addLast("transferHandler", transferHandler)
                                .addLast("writabilityChangedHandler", writabilityChangedHandler)
                                .addLast("exceptionHandler", exceptionHandler);
                    }
                });

        this.natClientService.getAllProxies()
                .forEach(natProxy -> bind(natProxy.getId(), natProxy.getProxyHost(), natProxy.getProxyPort()));
    }

    @Override
    public void stop() {
        this.channelMap.forEach((aLong, channel) -> channel.close());

        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }

    public void bind(long proxyId, String proxyHost, int proxyPort) {

        ChannelFuture f;
        if ("*".equals(proxyHost)) {
            f = this.serverBootstrap.bind(proxyPort);
        } else {
            f = this.serverBootstrap.bind(proxyHost, proxyPort);
        }

        channelMap.put(proxyId, f.channel());
        log.info("proxy server started success. host:{}, port:{}", proxyHost, proxyPort);
    }

    public void unbind(long proxyId) {
        Channel channel = channelMap.remove(proxyId);
        if (channel != null) {
            channel.close();
        }

    }
}
